package com.starry.core.web.core.web.execption;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.starry.core.common.constants.CommonError;
import com.starry.core.common.domain.R;
import com.starry.core.common.execption.ServiceException;
import com.starry.core.common.utils.ServletUtils;
import com.starry.core.common.utils.UserAgentParser;
import com.starry.core.ip.core.utils.IPUtils;
import com.starry.core.security.context.SecurityInfoContext;
import com.starry.module.system.api.logger.SysLogErrorApi;
import com.starry.module.system.api.logger.domain.qo.SysLogErrorQo;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.time.LocalDateTime;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @Description 全局异常管理
 * @Author xiaoke
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


    private final SysLogErrorApi sysLogErrorApi;

    @Value("${spring.application.name}")
    private String applicationName;

    public GlobalExceptionHandler(SysLogErrorApi sysLogErrorApi) {
        this.sysLogErrorApi = sysLogErrorApi;
    }

    /**
     * 处理所有异常，主要是提供给 Filter 使用
     * 因为 Filter 不走 SpringMVC 的流程，但是我们又需要兜底处理异常，所以这里提供一个全量的异常处理过程，保持逻辑统一。
     *
     * @param request 请求
     * @param ex 异常
     * @return 通用返回
     */
    public R<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
        if (ex instanceof MissingServletRequestParameterException) {
            return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
        }
        if (ex instanceof MethodArgumentTypeMismatchException) {
            return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
        }
        if (ex instanceof MethodArgumentNotValidException) {
            return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
        }
        if (ex instanceof BindException) {
            return methodArgumentNotValidException((BindException) ex);
        }
        if (ex instanceof NoHandlerFoundException) {
            return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
        }
        if (ex instanceof NoResourceFoundException) {
            return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex);
        }
        if (ex instanceof HttpRequestMethodNotSupportedException) {
            return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
        }
        if (ex instanceof ServiceException) {
            return doServiceException((ServiceException) ex);
        }
        if (ex instanceof AccessDeniedException) {
            return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
        }
        return doException(request, ex);
    }

    /**
     * 处理 SpringMVC 请求参数缺失
     * 例如说，接口上设置了 @RequestParam("xx") 参数，结果并未传递 xx 参数
     */
    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    public R<Void> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
        log.warn("[missingServletRequestParameterExceptionHandler]", ex);
        return R.fail(HttpStatus.BAD_REQUEST.value(), String.format("请求参数缺失:%s", ex.getParameterName()));
    }

    /**
     * 处理 SpringMVC 请求参数类型错误
     * 例如说，接口上设置了 @RequestParam("xx") 参数为 Integer，结果传递 xx 参数类型为 String
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public R<Void> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
        log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex);
        return R.fail(HttpStatus.BAD_REQUEST.value(), String.format("请求参数类型错误:%s", ex.getMessage()));
    }

    /**
     * 处理 SpringMVC 参数校验不正确
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<Void> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
        log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
        FieldError fieldError = ex.getBindingResult().getFieldError();
        assert fieldError != null; // 断言，避免告警
        return R.fail(HttpStatus.BAD_REQUEST.value(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
    }


    /**
     * 处理 SpringMVC 请求地址不存在
     * 注意，它需要设置如下两个配置项：
     * 1. spring.mvc.throw-exception-if-no-handler-found 为 true
     * 2. spring.mvc.static-path-pattern 为 /statics/**
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public R<Void> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
        log.warn("[noHandlerFoundExceptionHandler]", ex);
        return R.fail(HttpStatus.NOT_FOUND.value(), String.format("请求地址不存在:%s", ex.getRequestURL()));
    }

    /**
     * 处理 SpringMVC 请求方法不正确
     * 例如说，A 接口的方法为 GET 方式，结果请求方法为 POST 方式，导致不匹配
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public R<Void> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
        log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex);
        return R.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), String.format("请求方法不正确:%s", ex.getMessage()));
    }

    /**
     * 处理 SpringMVC 请求地址不存在
     */
    @ExceptionHandler(NoResourceFoundException.class)
    private R<?> noResourceFoundExceptionHandler(HttpServletRequest req, NoResourceFoundException ex) {
        log.warn("[noResourceFoundExceptionHandler]", ex);
        return R.fail(HttpStatus.NOT_FOUND.value(), String.format("请求地址不存在:%s", ex.getResourcePath()));
    }

    /**
     * 处理 SpringMVC 请求参数类型错误
     * 例如说，接口上设置了 @RequestBody实体中 xx 属性类型为 Integer，结果传递 xx 参数类型为 String
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public R<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
        log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
        if (ex.getCause() instanceof InvalidFormatException invalidFormatException) {
            return R.fail(HttpStatus.BAD_REQUEST.value(), String.format("请求参数类型错误:%s", invalidFormatException.getValue()));
        } else {
            return doException(ServletUtils.getRequest(), ex);
        }
    }

    /**
     * 处理 Spring Security 权限不足的异常
     * 来源是，使用 @PreAuthorize 注解，AOP 进行权限拦截
     */
    @ExceptionHandler(value = AccessDeniedException.class)
    public R<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
        log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", SecurityInfoContext.getUserId(),
                req.getRequestURL(), ex);
        return R.fail(HttpStatus.FORBIDDEN.value(), "没有权限访问该资源");
    }

    /**
     * 参数校验异常
     */
    @ExceptionHandler(BindException.class)
    public R<Void> methodArgumentNotValidException(BindException e) {
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        String messages = allErrors.stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(", ", "[", "]"));
        return R.fail(HttpStatus.BAD_REQUEST.value(), CommonError.REQUEST_PARAM_NULL.getMsg() + ": " + messages);
    }

    /**
     * 业务异常
     */
    @ExceptionHandler(ServiceException.class)
    public R<Void> doServiceException(ServiceException e) {
        try {
            StackTraceElement[] stackTraces = e.getStackTrace();
            for (StackTraceElement stackTrace : stackTraces) {
                if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceException.class.getName())) {
                    log.warn("[业务异常]\n\t{}", stackTrace);
                    break;
                }
            }
        } catch (Exception ignored) {
            // 忽略日志，避免影响主流程
        }
        return R.fail(e.getCode(), e.getMessage());
    }

    /**
     * 系统异常
     */
    @ExceptionHandler(Exception.class)
    public R<Void> doException(HttpServletRequest req, Throwable ex) {

        log.error("系统异常", ex);

        // 记录异常日志
        createExceptionLog(req, ex);

        return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), CommonError.SYSTEM_ERROR.getMsg());
    }

    private void createExceptionLog(HttpServletRequest req, Throwable ex) {
        SysLogErrorQo logError = new SysLogErrorQo();
        try {
            // 初始化 errorLog
            buildExceptionLog(logError, req, ex);
            // 执行插入 errorLog
//            sysLogErrorApi.save(logError);
        } catch (Throwable th) {
            log.error("[记录异常日志][url({}) log({}) 发生异常]", req.getRequestURI(), logError, th);
        }
    }

    private void buildExceptionLog(SysLogErrorQo logError, HttpServletRequest request, Throwable e) {

        // 用户信息
        logError.setOperId(SecurityInfoContext.getUserId());
        logError.setOperName(SecurityInfoContext.getUsername());

        // 异常信息
        logError.setExceptionName(e.getClass().getName());
        logError.setExceptionMessage(ExceptionUtil.getMessage(e));
        logError.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
        logError.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));

        // 异常堆栈
        StackTraceElement[] stackTraceElements = e.getStackTrace();
        Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
        StackTraceElement stackTraceElement = stackTraceElements[0];
        logError.setExceptionClassName(stackTraceElement.getClassName());
        logError.setExceptionFileName(stackTraceElement.getFileName());
        logError.setExceptionMethodName(stackTraceElement.getMethodName());
        logError.setExceptionLineNumber(stackTraceElement.getLineNumber());

        logError.setApplicationName(applicationName);

        // 请求体
        Map<String, Object> requestParams = MapUtil.<String, Object>builder()
                .put("query", JakartaServletUtil.getParamMap(request))
                .put("body", JakartaServletUtil.getBody(request)).build();
        logError.setRequestBody(JSONUtil.toJsonStr(requestParams));
        logError.setRequestUrl(request.getRequestURI());


        //请求头
        logError.setRequestHeader(getRequestHeader(request));

        // 操作时间
        logError.setOperTime(LocalDateTime.now());

        // ip
        logError.setRequestIp(IPUtils.getIpAddr(request));
        try {
            logError.setIpAddress(IPUtils.getArea(logError.getRequestIp()).getName());
        } catch (Exception ignored) {
        }

        // 设置浏览器类型
        UserAgentParser.UserAgentInfo userAgentInfo = UserAgentParser.parseUserAgent(request);
        logError.setBrowserType(userAgentInfo.getBrowser());
        logError.setOperatingSystem(userAgentInfo.getOs());
    }

    /**
     * 获取请求头
     * @param request request
     * @return json
     */
    private String getRequestHeader(HttpServletRequest request) {
        Map<String, String> headerMap = new LinkedHashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            headerMap.put(headerName, headerValue);
        }
        return JSONUtil.toJsonStr(headerMap);
    }

}
